JS toFixed에 대해서
특정 소수점자리까지 반올림해주는 Number객체의 메서드인 toFixed가 있다. 한번 사용해보자.
console.log((1.55).toFixed(1)) // 1.6
console.log((1.65).toFixed(1)) // 1.6
??
처음에는 반올림 사사오입, 오사오입과 관련된 문제인 줄 알았다.
toFixed가 banker's rounding방식으로 구현되었구나 생각했는데 나의 착각이였다.
console.log((10.65).toFixed(1)) //10.7
TC39에서 정의된 Number.toFixed 부분을 찾아보았다.
1. Let x be ? ThisNumberValue(this value).
2. Let f be ? ToIntegerOrInfinity(fractionDigits).
3. Assert: If fractionDigits is undefined, then f is 0.
4. If f is not finite, throw a RangeError exception.
5. If f < 0 or f > 100, throw a RangeError exception.
6. If x is not finite, return Number::toString(x, 10).
7. Set x to ℝ(x).
8. Let s be the empty String.
9. If x < 0, then
a. Set s to "-".
b. Set x to -x.
10. If x ≥ 10**21, then
a. Let m be ! ToString(𝔽(x)).
11. Else,
a. Let n be an integer for which n / 10**f - x is as close to zero as possible. If there are two such n, pick the larger n.
b. If n = 0, let m be "0". Otherwise, let m be the String value consisting of the digits of the decimal representation of n (in order, with no leading zeroes).
c. If f ≠ 0, then
i. Let k be the length of m.
ii. If k ≤ f, then
1. Let z be the String value consisting of f + 1 - k occurrences of the code unit 0x0030 (DIGIT ZERO).
2. Set m to the string-concatenation of z and m.
3. Set k to f + 1.
iii. Let a be the first k - f code units of m.
iv. Let b be the other f code units of m.
v. Set m to the string-concatenation of a, ".", and b.
12. Return the string-concatenation of s and m.
출처 : https://tc39.es/ecma262/#sec-number.prototype.tofixed
요약하자면 x.toFixed(f) 일 때,
단, 그러한 n이 여러개 인 경우 큰 값으로 고른다.
즉, toFixed는 rounding half of even이 아닌 rounding half up 방식을 사용하고 있었다.
문제는 바로 너 floating point
또 너야 JS 라고 생각하며 이유를 검색해봤는데, 이번 꺼는 나의 억까였다.
컴퓨터의 부동소수점 표현에 관련된 것이었다.
JS는 수를 64bit 부동소수점으로 표현한다.
1.65를 64bit 부동소수점으로 표현하면 다음과 같다.
1.65를 부동소수점으로는 정확하게 표현할 수가 없기 때문에 위와 같이 1.64999.. 로 저장된다.
이를 위에서 본 toFixed의 명세에 대입해서 생각해보면,
- n=16일 때
- n=17일 때
이 되고 n은 0에 더 가까운 16이 되어서 1.7이 아닌 1.6이 된다.
즉, 결론은 floating point의 부정확한 소수표현 때문이었다.
해결책
더 정확한 toFixed를 위해서는 어떻게 할 수 있을까?
stack overflow 참고
// x.toFixed(f)
Math.round((x + Number.EPSILON) * 10**f) / 10**f
예시
console.log((1.654345).toFixed(5)) //1.65434
console.log(Math.round((1.654345 + Number.EPSILON) * 10**5) / 10**5) // 1.65435
이외에도 다양한 방법들이 있다.
또한 라이브러리를 사용할 수도 있다.
Math.round에서는?
우선 round의 반올림 방식에 대해서 찾아봤다.
TC39 - Math.round 에 명세돼있길.
Let n be ? ToNumber(x).
2. If n is not finite or n is an integral Number, return n.
3. If n < 0.5𝔽 and n > +0𝔽, return +0𝔽.
4. If n < -0𝔽 and n ≥ -0.5𝔽, return -0𝔽.
5. Return the integral Number closest to n, preferring the Number closer to +∞ in the case of a tie.
0.5의 경우
Math.round는 toFixed와 달리 부동소수점 오차에 영향을 받지 않는다.
0.5는 부동소수점으로 표현해도 정확하게 표현될 수 있기 떄문이다.
(사실 위에 경우는 32bit 부동소수점 표현이긴한데 비슷하다)
이러한 이유로 Math.round는 부동소수점 표현으로 이한 오차가 발생하지 않는다.
결론
소수의 정확성을 항상 의심하자